#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QMessageBox>
#include "frasclient.h"
#include "ui_frasclient.h"

FrasClient::FrasClient(QWidget *parent)
    : QMainWindow(parent), ui(new Ui::FrasClient) {
    ui->setupUi(this);
    // 打开摄像头，参数为打开索引（只有一个摄像头，Linux下需要改成/dev/video0~...）
    if (!cap_.open(0)) {
        QMessageBox::critical(this, "错误", "摄像头开启失败！");
        return;
    }
    // 导入级联分类器文件（这个文件是一个训练好的脸部特征数据）
    if (!cascade_.load("D:/opencv/build/etc/haarcascades/haarcascade_frontalface_alt2.xml")) {
        QMessageBox::critical(this, "错误", "加载级联分类器文件失败！");
        return;
    }

    // 信号槽连接相关逻辑
    // QTcpSocket在连接成功时会发送connected信号，断开连接时会发送disconnected信号，这里通过这两个信号来控制定时器的启动和停止
    connect(&socket_, &QTcpSocket::disconnected, this, [this](){
        conn_timer_.start(5000);  // 启动自动连接定时器，用于定时发送信号与服务器建立连接
        qDebug() << "检测到连接断开，尝试重连中...";
    });  // 断开连接时，尝试自动重连
    connect(&socket_, &QTcpSocket::connected, this, [this](){
        conn_timer_.stop();       // 停止定时器
        qDebug() << "连接成功！\n";
    });  // 连接成功时，停止定时器的自动连接
    connect(&conn_timer_, &QTimer::timeout, this, [this](){
        socket_.connectToHost("127.0.0.1", 15213);
        qDebug() << "尝试与服务器建立连接...\n";
    });  // 定时器连接服务器（定期自动连接服务器）
    // 关联从服务器接受数据的槽函数
    connect(&socket_, &QTcpSocket::readyRead, this, &FrasClient::recvFromServer);

    ui->notifWG->hide();      // 暂时隐藏消息提示窗口
    startTimer(100);          // 启动定时器事件，间隔为100ms（每100ms采集一帧数据，注意将该定时器与下面的定时器区分开）
    conn_timer_.start(5000);  // 定时器间隔设置为5秒（连接成功后就不会再连接）
    flag_ = 0;
}

FrasClient::~FrasClient() {
    if (cap_.isOpened()) {  // 关闭摄像头
        cap_.release();
    }
    delete ui;
}

void FrasClient::timerEvent(QTimerEvent *e) {
    if (!cap_.isOpened()) {
        return;
    }
    // 采集数据
    cv::Mat frame;
    cap_ >> frame;        // 读取一帧数据
    if (frame.empty()) {  // 未采集到数据
        return;
    }
    // 检测人脸数据
    cv::Mat gray_frame;
    cv::cvtColor(frame, gray_frame, cv::COLOR_BGR2GRAY);  // 将原始帧转成灰度图以加快检测速度
    std::vector<cv::Rect> faceRects;                     // 定义矩形向量组用于存放人脸检测结果
    /**
     * detectMultiScale函数说明：
     * 功能：在输入图像中检测多个尺度上的对象，并返回检测到的对象的边界框（矩形）
     * param1：image——待检测图片，一般为灰度图像以加快检测速度
     * param2：object——被检测物体的矩形框向量组
     * param3：scaleFactor——在前后两次相继的扫描中，搜索窗口的比例系数，默认为1.1，即搜索窗口每次扩大10%
     * param4：minNeighbors——构成检测目标的相邻矩形的最小个数（默认为三个），
     *        组成检测目标的小矩形的个数和小于该值-1时会被排除，若该值设为0，则函数不做任何操作就返回所有的被检候选矩形框
     *        此类设定值一般用在用户自定义对检测结果的组合程序上
     * param5：flag——使用默认值或CV_HAAR_DO_CANNY_PRUNING，使用后者时函数将会用Canny边缘检测来排除边缘过多或过少的区域，
     *         因此这些区域通常不会是人脸所在区域
     * param6/7：minSize/maxSize——限制所获取的目标区域范围
     */
    cascade_.detectMultiScale(gray_frame, faceRects);  // 后面全部使用默认值
    if (faceRects.size() > 0 && flag_ >= 0) {  // 限制同一张人脸在离开摄像头区域之前的发送次数（性能优化点）
        // 检测到人脸，获取单个人脸的矩形框并保存（此处每次只需要识别单个人脸）
        cv::Rect rec = faceRects.at(0);
        ui->recgimgLB->move(rec.x, rec.y);           // 移动人脸检测框（图片——QLabel）
        if (flag_ > 2) {
            // 将数据编码为jpg格式并转化为字节数组
            std::vector<uchar> buf;            // 存编码后的数据
            cv::imencode(".jpg", frame, buf);  // 进行编码，其中第四个参数是编码参数（编码flag和编码质量），此处直接采用默认值
            QByteArray ba(reinterpret_cast<const char *>(buf.data()), buf.size());
            // 将处理好的数据发送到服务器
            QByteArray sendData;
            QDataStream stream(&sendData, QIODevice::WriteOnly);  // 只写
            quint64 ba_size = ba.size();
            stream.setVersion(QDataStream::Qt_5_15);
            stream << ba_size << ba;     // 数据写入到流中（先发大小，再发数据）
            socket_.write(sendData);
            flag_ = -2;                   // 设为-2使得当前人脸离开摄像头区域前不再发送数据
            // 获取人脸数据并保存
            auto tmp_face = frame(rec);
            cv::imwrite("./tmpface.jpg", tmp_face);// 临时保存到当前路径用于后续使用
        }
        flag_++;
    }
    if (faceRects.empty()) {  // 未检测到人脸时，需要让人脸框停留在默认位置
        ui->recgimgLB->move(180, 70);
        flag_ = 0;  // 清空人脸标识表示可以刷新发送状态
    }

    // 将opencv中的Mat数据（BGR格式）转成QImage数据（RGB格式）
    cv::cvtColor(frame, frame, cv::COLOR_BGR2RGB);
    // 确保图像数据连续
    if (!frame.isContinuous()) {
        frame = frame.clone();
    }
    QImage img(frame.data, frame.cols, frame.rows, frame.step, QImage::Format_RGB888);  // 创建图像
    // 通过放大和裁剪图像使其尺寸与对应的标签控件相匹配（使用平滑插值算法以减少缩放后的图像失真和锯齿现象）
    img = img.scaled(ui->videoLB->size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
    int ow = img.width();
    int oh = img.height();
    int sx = (ow - 800) / 2;
    int sy = (oh - 800) / 2;
    img = img.copy(sx, sy, 800, 800);
    // 生成并显示图像
    ui->videoLB->setPixmap(QPixmap::fromImage(img));
}

void FrasClient::recvFromServer() {
    // 接收并解析服务端发来的JSON数据
    QByteArray data = socket_.readAll();
    // 返回Json数据为空串时说明考勤失败
    if (*data.data() == 0) {
        qDebug() << "FrasClient::recvFromServer(): 考勤失败，未查询到人脸注册信息！\n";
        return;
    }
    QJsonParseError err;
    QJsonDocument doc = QJsonDocument::fromJson(data, &err);
    if (err.error != QJsonParseError::NoError) {  // Json数据出现错误
        qDebug() << "FrasClient::recvFromServer(): Json数据格式错误\n";
        return;
    }
    // 将信息输出到控件
    QJsonObject obj = doc.object();
    ui->txt1->setText(obj.value("staff_id").toString());
    ui->txt2->setText(obj.value("staff_name").toString());
    ui->txt3->setText(obj.value("staff_depa").toString());
    ui->txt4->setText(obj.value("time").toString());
    // 通过设置样式来显示头像（这种方式可以将头像显示为圆形以适配控件）
    ui->headerLB->setStyleSheet("border-image:url(./tmpface.jpg)");
    ui->notifWG->show();  // 显示消息提示栏
    // 定时器清空窗口信息，间隔为5秒
    QTimer::singleShot(5000, this, [this](){
        ui->txt1->clear();
        ui->txt2->clear();
        ui->txt3->clear();
        ui->txt4->clear();
        ui->headerLB->setStyleSheet("");
        ui->notifWG->hide();
    });
}
